//-----------------------------------------------------------------------
// <copyright file="DragDropManager.cs" company="n.a.">
//     Copyright Dave999. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
[module: System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "To be returned to .")]

/*
 *  Todo: 1. Refactor event handler names 2. selection mode management.
 */
/// <summary>
/// Summary description for Class1
/// </summary>
namespace DbView
{
	using System;
	using System.Collections;
	using System.Drawing;
	using System.Drawing.Imaging;
	using System.Windows.Forms;

	#region DragDropConnection class
	/* 
     * Class to hold information about a logical drag drop operation 
     * between two listboxes.
     * The source holds the item to transfer and the target retrieves it.
     * The operation may be either a copy operation in which case the item
     * will not be removed from the source list box, or a move operation in
     * which case it is.
     * A special case is where the source and target list boxes are the same. In
     * this case the operation is assumed to be a move (or re-order operation).
    */
	public class DragDropConnection
	{
		public ListBox source;
		public ListBox target;
		public DragDropEffects Effects;  // either move or copy (see overview)
		public bool deleteOnDrop;        // Set automatically when source and target are 
		// different (copy = false, move = true)
		private SelectionMode HoldTargetMode; // holds original target selection mode as set in design time

		// construct with source and target listboxes and type of operation
		public DragDropConnection(ListBox sourceIn, ListBox targetIn, DragDropEffects effectsIn)
		{
			this.source = sourceIn;
			this.target = targetIn;
			this.Effects = effectsIn;
			this.deleteOnDrop = false;
			this.HoldTargetMode = targetIn.SelectionMode;
		}
		// Accessors for the manager class 
		public ListBox Source { get { return this.source; } }
		public ListBox Target { get { return this.target; } }

		// Accessor and setter. 
		public bool DeleteOnDrop
		{
			get { return this.deleteOnDrop; }
			set { this.deleteOnDrop = value; }
		}

		// helper. Returns T if the input matches the source list box
		public bool MatchesSource(ListBox lbComp)
		{
			return (lbComp == this.source) ? true : false;
		}

		// helper. Returns T if the input matches the targt list box
		public bool MatchesTarget(ListBox lbComp)
		{
			return (lbComp == this.target) ? true : false;
		}

		// helper. Returns T if the the object represents a reorder operation
		public bool TargetIsSource()
		{
			return (this.source == this.target) ? true : false;
		}

		public bool isCopyConnection()
		{
			return ((this.Effects & DragDropEffects.Copy) == DragDropEffects.Copy) ? true : false;
		}

		public bool isMoveConnection()
		{
			return ((this.Effects & DragDropEffects.Move) == DragDropEffects.Move) ? true : false;
		}

		public void OnStartSession()
		{
			this.target.SelectionMode = SelectionMode.One;
		}

		public void OnEndSession()
		{
			this.target.SelectionMode = this.HoldTargetMode;
		}

	}
	#endregion

	#region Notification
	/*
     * Event status to be sent to client either on the start or finish of
     * a drag drop event. The client can cancel the connection or drop
     * operation to prevent the item being transferred
     */
	public class DragDropNotifyEventArgs : EventArgs
	{
		public ListBox Source;          // the source listBox from the MouseDown event
		public ListBox Target;          // the target listBox from the MouseDown event
		public int DropItemIndex;       // the index of the listbox item in the source to drop
		public bool Cancel;             // item to set to signal cancel of drop

		public DragDropNotifyEventArgs(ListBox sourceIn, ListBox targetIn, int dropItemIdx)
		{
			Source = sourceIn;
			Target = targetIn;
			DropItemIndex = dropItemIdx;
			Cancel = false;
		}
	}
	// define the event handler callback
	public delegate void DragDropNotify(object sender, DragDropNotifyEventArgs e);

	#endregion

	/*
     * Class to implement the fiddly logic of a drag-drop operation. Respon sibilities
     * 1. creating all the drag drop event handlers.
     * 2. managing a list of connected listboxes with a variety of possible
     *    dragdrop behaviours.
     */
	public class DragDropManager
	{
		private bool processMouseDoubleClick; // flag to set whether to implement the MouseDoubleClick event handler

		private bool active;             // true when in a drag drop operation false otherwise
		private bool dragCancelled;      // true when ESC pressed during a drag operation false otherwise
		private ListBox source;          // the source listBox from the MouseDown event
		private int dragItemIndex;       // index of item to copy or move.
		private MouseEventArgs e;        // the Mouse location from the MouseDown event
		private long scrollDelay;        // variable for a scroll when the potential drop target requires scrolling
		//                          
		private ArrayList ConnectionList; // list of DragDropConnection objects representing
		// the logical wiring of the form for drag and drop
		// event handler to supply feedback to client event
		public event DragDropNotify Notify; // event to allow client some control 

		public DragDropManager(bool handleDoubleClick)
		{
			this.ConnectionList = new ArrayList();
			this.processMouseDoubleClick = handleDoubleClick;
		}

		// Property. Option to process MouseDoubleClicks for dragdrop logic
		public bool HandlesDoubleClick
		{
			get { return this.processMouseDoubleClick; }
			// nb: set before configuration. won't change behaviour after
			// set { this.processMouseDoubleClick = value; }
		}


		// Helper: connects the event handlers for a dragdrop source
		private void RegisterSourceBox(ListBox listBox)
		{
			listBox.MouseDown += new MouseEventHandler(this.HandleListBoxMouseDown);
			listBox.MouseMove += new MouseEventHandler(this.HandleListBoxMouseMove);
			listBox.MouseUp += new MouseEventHandler(this.HandleListBoxMouseUp);
			listBox.QueryContinueDrag += new QueryContinueDragEventHandler(this.HandleListBoxQueryContinueDrag);
			//listBox.GiveFeedback += new GiveFeedbackEventHandler(this.listBox1_GiveFeedback);
			if (this.processMouseDoubleClick)
				listBox.MouseDoubleClick += new MouseEventHandler(this.HandleListBoxMouseDoubleClick);
		}

		// Helper: connects the event handlers for a dragdrop destination or target
		private void RegisterTargetBox(ListBox listBox)
		{
			listBox.DragOver += new DragEventHandler(this.HandleListBoxDragOver);
			listBox.DragDrop += new DragEventHandler(this.HandleListBoxDragDrop);
			listBox.DragEnter += new DragEventHandler(this.HandleListBoxDragEnter);
			if (this.processMouseDoubleClick)
				listBox.MouseDoubleClick += new MouseEventHandler(this.HandleListBoxMouseDoubleClick);
		}

		// Helper: locates connection information for the current context
		private DragDropConnection Find(ListBox source, ListBox target)
		{
			// search all connections
			foreach (DragDropConnection dragConn in this.ConnectionList)
			{
				// return the connection object when source and target match inputs
				if (dragConn.MatchesSource(source) && dragConn.MatchesTarget(target))
					return dragConn;
			}
			return null;
		}

		// Helper: find where to restore a deleted item
		private DragDropConnection FindMoveSource(ListBox target)
		{
			// search all connections
			foreach (DragDropConnection dragConn in this.ConnectionList)
			{
				// if this is a Move relationship
				if (dragConn.Effects == DragDropEffects.Move)
					// found an item that matches drop target
					if (dragConn.MatchesTarget(target))
						// don't proceed if it also matches the source
						// (different logic is required for moving within a listbox)
						if (!dragConn.MatchesSource(target))
							// all criteria satisfied. 
							// the calling method will populate the source with the removed item
							return dragConn;
			}
			return null;
		}

		// Helper: called on the start of a drag-drop sets the
		//         target AllowDrop property T if it is configured 
		//         to receive items from the current source listbox
		private void SetTargetState(ListBox currentSource)
		{
			// step 1 turn off all targets that are connected to the source listbox
			foreach (DragDropConnection dragConn in this.ConnectionList)
			{
				if (!dragConn.MatchesSource(this.source))
					dragConn.Target.AllowDrop = false;
			}
			// step 2 turn on all targets that are connected to the source
			foreach (DragDropConnection dragConn in this.ConnectionList)
			{
				if (dragConn.MatchesSource(this.source))
					dragConn.Target.AllowDrop = true;
			}
		}

		// Register pairs of list box controls for drag and drop operations.
		// source and target are self-evident. The DragDropEffects parameter
		// should be set to either Copy or Move. If Move then the dragged item will be
		// removed from the source. If copy then it will remain in the source
		// Note: with Move then if the item is removed from the target it will be
		//       restored to the source.
		// Note: where source and target are the same listbox then this allows items
		//       to be moved.
		// 
		public void ConfigureDragDrop(ListBox source, ListBox target, DragDropEffects effects)
		{
			DragDropConnection conn = new DragDropConnection(source, target, effects);
			// test whether the control needs to attach the event handlers yet
			// Independent tests for source or target handlers.
			bool regSource = true;
			bool regTarget = true;
			foreach (DragDropConnection dragConn in this.ConnectionList)
			{
				if (dragConn.MatchesSource(source))
					regSource = false;
				if (dragConn.MatchesTarget(target))
					regTarget = false;
			}
			if (regSource)
				this.RegisterSourceBox(source);
			if (regTarget)
				this.RegisterTargetBox(target);
			// test effects and set delete flag
			// if you configure for move then expect to lose the item
			//  from the source list. If you configure for copy then
			// the item will remain in the source list
			if (effects == DragDropEffects.Move)
				conn.DeleteOnDrop = true;

			this.ConnectionList.Add(conn);
		}

		// Registers a listBox as containing movable content.
		// If deleteOnDropOutside is set to true then dragging item outside the
		//  listbox will remove it. if false then no action 
		public void EnableDragDropMove(ListBox source, bool deleteOnDropOutside)
		{
			this.ConfigureDragDrop(source, source, DragDropEffects.Move);
			DragDropConnection dragConn = this.Find(source, source);
			if (dragConn != null)
				dragConn.DeleteOnDrop = deleteOnDropOutside;
		}

		// helper: set when [ESC] is pressed during a drag operation
		public void CancelCurrentOperation()
		{
			this.active = false;
			this.source = null;
			this.e = null;
			this.scrollDelay = 0;
			this.dragCancelled = true; // set to true to prevent action when the mouse is released
		}

		// available to my successors for tracing event sequences
		private void trace(String s)
		{
			return; // no trace
			//DateTime dt = DateTime.Now;
			//System.Console.WriteLine("{0}: {1}", dt.ToString(), s);
		}

		/*
		 * static drag drop helpers
		 */

		// Updates the target by placing the item at the given location
		public static void UpdateTarget(ListBox listBox, string item, int insertAt)
		{
			bool appending = (insertAt == -1);  // item will be appended
			if (appending)
				insertAt = listBox.Items.Count; // this adjustment makes the logic below easier
			// find current position of item in list (-1 if not present)
			int currentPos = listBox.Items.IndexOf(item);
			bool newItem = (currentPos == -1);
			// set flag if item is moving down the list
			bool movingDown = (!newItem && (insertAt > currentPos));
			// apply logic
			// if no change 
			if (insertAt == currentPos)
			{
				// no action dropping in old position
				currentPos += 0; // this is a no-change statement to purge a compiler warning
			}
			else
			{
				// test for item moving down
				if (movingDown)
				{
					// increase insertion point to account for removing the original
					++insertAt;
					// test edge case where moving to the end
					if (insertAt >= listBox.Items.Count)
						appending = true;
				}
				// add the item
				if (appending)
					listBox.Items.Add(item);
				else
					listBox.Items.Insert(insertAt, item);
				// set the index to the dropped item
				// listBox.SelectedIndex = insertAt;                        
				// find item to remove
				if (!newItem)
					if (movingDown)
						listBox.Items.RemoveAt(currentPos);
					else
						listBox.Items.RemoveAt(currentPos + 1);

				listBox.SelectedIndex = listBox.Items.IndexOf(item);
			}
		}

		public static void RemoveFromList(ListBox listBox, string s)
		{
			int removeAt = listBox.Items.IndexOf(s);
			if (removeAt != -1)
			{
				listBox.Items.RemoveAt(removeAt);
				removeAt -= (removeAt >= listBox.Items.Count) ? 1 : 0;
				listBox.SelectedIndex = removeAt;
			}
		}

		// invoked on mouse down to start a potential dragdrop session
		// (this session will abandoned if a mouse doubleclick arises)
		// 
		// invoked on mouse down to start a potential dragdrop session
		// (this session will abandoned if a mouse doubleclick arises)
		// 
		private void startSession(ListBox listBox, MouseEventArgs e)
		{
			this.active = true;
			this.dragCancelled = false;
			this.source = listBox;
			this.e = e;
			this.dragItemIndex = listBox.IndexFromPoint(e.X, e.Y);
		}
		// helper: invoked when starting a drag operation
		private void startDrag()
		{
			this.scrollDelay = 0;
			// loop around the registered listboxes and set accept state
			this.SetTargetState(this.source);
			// set selection mode to single on all targets  
			foreach (DragDropConnection dragConn in this.ConnectionList)
				dragConn.OnStartSession();
		}
		// helper: invoked when starting a drag operation
		private void endDrag()
		{
			this.dragCancelled = false;
			// set selection mode to single on all targets  
			foreach (DragDropConnection dragConn in this.ConnectionList)
				dragConn.OnEndSession();
		}


		// helper: set when [ESC] is pressed during a drag operation
		public void endSession()
		{
			this.endDrag(); // roll up drag cancel logic into end session logic for now.
			this.active = false;
			this.source = null;
			this.e = null;
			this.scrollDelay = 0;
		}

		// Notify client when a drag operation completes
		protected virtual void OnNotify(DragDropNotifyEventArgs e)
		{
			if (this.Notify != null)
				this.Notify(this, e);
		}

		/*
		 *  source list box event handlers
		*/

		// Generic handler for a source listBox MouseDown event
		private void HandleListBoxMouseDown(object sender, MouseEventArgs e)
		{
			this.trace("OnListBoxMouseDown");
			// find the listbox 
			ListBox listBox = (ListBox)sender;
			// if empty then no action is neccessary
			if (listBox.Items.Count == 0)
				return;
			// if the mouse is not over an item then no action is neccessary 
			int itemIndex = listBox.IndexFromPoint(e.X, e.Y);
			if (itemIndex == -1)
				return;
			// Good to go.
			// Log state for use in the Mousemove event handler.
			this.startSession(listBox, e);
		}

		// Generic handler for a source listBox MouseUp event
		private void HandleListBoxMouseUp(object sender, MouseEventArgs e)
		{
			this.trace("OnListBoxMouseUp");
			// cancel any potential session 
			this.endSession();
		}

		// generic handler for source MouseMove event
		private void HandleListBoxMouseMove(object sender, MouseEventArgs e)
		{
			// identify the source (should always be equal to this.source
			ListBox listBox = (ListBox)sender;
			// if either condition is set then dragging is not relevant
			if (listBox.Items.Count == 0 || this.active == false)
				return;
			// test for deviation from position at MouseDown
			// this discards a small MouseMovement during and intended
			// DoubleClick.
			if (Math.Abs(e.X - this.e.X) <= SystemInformation.DragSize.Width &&
				Math.Abs(e.Y - this.e.Y) <= SystemInformation.DragSize.Height)
				return;

			// Good to go.
			this.trace("listBox_MouseMove"); // only trace here or you will be swamped
			// Obtain the item data using the index identified during the MouseDown event.
			string s = listBox.Items[this.dragItemIndex].ToString();
			// may set target list box state
			this.startDrag();
			// Inoke the system dragDrop logic
			// need to find all targets and set correct move/copy
			DragDropEffects dde1 = listBox.DoDragDrop(s, DragDropEffects.Copy | DragDropEffects.Move);

			// back after the operation completes / terminates
			// test our cancelled flag. Do nothing if it is set
			if (this.dragCancelled == false)
			{
				// this if item is dropped outside a target
				if (dde1 == DragDropEffects.None)
				{
					// determine whether this is the result of a reorder/remove 
					// ie. the source and target list boxes are the same
					// search for a source == target 
					DragDropConnection dragConn = this.Find(this.source, listBox);
					// Non-null means yes it is.
					if (dragConn != null)
						// if dragging an item outside the control will delete it ...
						if (dragConn.DeleteOnDrop)
						{
							// ... delete it
							RemoveFromList(listBox, s);
							// search for a source from which this item was moved.
							// that is a DragDropConnection where the source != this listbox
							// and the DragDropEffects was set to Move rather than Copy
							dragConn = this.FindMoveSource(listBox);
							if (dragConn != null)       // found
								dragConn.Source.Items.Add(s); // restore to source (at the end unfortunately)
						}
				}
			}
			// mark the end of the current session
			this.endSession();
		}

		// Generic handler for a source listBox QueryContinueDrag event
		private void HandleListBoxQueryContinueDrag(object sender, QueryContinueDragEventArgs e)
		{
			// test whether this event was triggered by the escape key
			// being pressed. This is the convention used for cancelling
			// the operation
			if (e.EscapePressed)
			{
				e.Action = DragAction.Cancel; // tell .net to cancel
				this.CancelCurrentOperation();     // set cancel flag for this object to use
			}
		}



		/*
		 *  Target list box event handlers
		*/

		// generic handler for taregt DragOver event
		private void HandleListBoxDragOver(object sender, DragEventArgs e)
		{
			this.trace("OnListBoxDragOver");
			// identify the control being dragged over
			ListBox listBox = (ListBox)sender;
			// translate mouse from screen to list box coords
			Point clientPoint = listBox.PointToClient(new Point(e.X, e.Y));
			// find the list box item at this point
			int iInsertion = listBox.IndexFromPoint(clientPoint);
			// highlight this to indicate the drop site
			listBox.SelectedIndex = iInsertion;
			// test whether to scroll for scroll up
			if (clientPoint.Y > listBox.Height - 10)  // mouse is within 10 pixels of the bottom
			{
				// if not already inserting in which case there is no need for scrolling
				if (iInsertion != -1)
				{
					// check whether we are in the scroll delay loop
					if (this.scrollDelay < DateTime.Now.Ticks)
					{
						// if not then scroll by incrementing the top item visible
						++listBox.TopIndex;
						// reset the scroll delay (2,000,000 = 2s I think)
						this.scrollDelay = DateTime.Now.Ticks + (2000000);
					}
				}
			}

			// scroll down
			else if (clientPoint.Y < 10)  // Mouse is withing 10 pixels of the top
			{
				if (listBox.TopIndex > 0) // can't go any further it first item is showing
				{
					if (this.scrollDelay < DateTime.Now.Ticks)
					{
						--listBox.TopIndex;
						// reset the scroll delay (2,000,000 = 2s I think)
						this.scrollDelay = DateTime.Now.Ticks + (2000000);
					}
				}
			}
		}

		// generic handler for release of a dragged object
		private void HandleListBoxDragDrop(object sender, DragEventArgs e)
		{
			this.trace("listBoxDragDrop");
			// find target listbox
			ListBox listBox = (ListBox)sender;
			// generate an event.
			DragDropNotifyEventArgs evt = new DragDropNotifyEventArgs(this.source, listBox, this.dragItemIndex);
			this.OnNotify(evt);
			// client has cancelled this for one reason or another
			if (evt.Cancel == true)
				return; // no further action

			// needs refactoring
			// move or copy the item
			// OnDDUpdateList(listBox, e);
			// test for presence of string data
			if (e.Data.GetDataPresent(DataFormats.StringFormat))
			{
				// item from source
				string item = (string)e.Data.GetData(DataFormats.StringFormat);
				// convert from screen to listbox co-ordinates
				Point p = listBox.PointToClient(new Point(e.X, e.Y));
				// find list position for insertion from mouse position
				int insertAt = listBox.IndexFromPoint(p);
				UpdateTarget(listBox, item, insertAt);
			}
			// test whether to delete from the source
			// this is where source != target and operation is Move, not Copy.
			if (e.Data.GetDataPresent(DataFormats.StringFormat))
			{
				// item from source
				string s = (string)e.Data.GetData(DataFormats.StringFormat);
				if (this.source != listBox) // when equal this is a move operation within a box
				{
					// search for the registered connection
					DragDropConnection dragConn = this.Find(this.source, listBox);
					if (dragConn != null)
						// if dragging an item outside the control will delete it ...
						if (dragConn.DeleteOnDrop)
							// ... delete it
							RemoveFromList(this.source, s); // may need to remove    
				}
			}
			return;
		}

		// Generic handler for DragEnter
		private void HandleListBoxDragEnter(object sender, DragEventArgs e)
		{
			// change the drag cursor to show that listbox2 is ready and set to accept data entry
			// either to Copy or Move
			// make sure we aren't inadvertently passing one of the other objects allowed such as a bitmap!
			ListBox listBox = (ListBox)sender;
			DragDropConnection dragConn = this.Find(this.source, listBox);
			if (dragConn != null)
				e.Effect = (dragConn.isCopyConnection()) ? DragDropEffects.Copy :
						   (dragConn.isMoveConnection()) ? DragDropEffects.Move : DragDropEffects.Copy;
			//e.Effect =  DragDropEffects.Copy;
			//trace(String.Format("{0}", e.Effect));
			/*
			if (e.Data.GetDataPresent(DataFormats.StringFormat) && (e.AllowedEffect == DragDropEffects.Copy))
				e.Effect = DragDropEffects.Copy;
			else
				e.Effect = DragDropEffects.Move;
			*/
		}

		/*
		 *  Mouse Double Click handler
		*/
		private void HandleListBoxMouseDoubleClick(object sender, MouseEventArgs e)
		{
			ListBox listBox = (ListBox)sender;
			// if session started then clear the session
			this.CancelCurrentOperation();
			// find item clicked on
			int itemIndex = listBox.IndexFromPoint(e.X, e.Y);
			if (itemIndex == -1)
				return;  // nothing to do
			// find the data clicked on
			string item = (String)listBox.Items[itemIndex];
			// find out what to do.
			bool processed = false;
			// 1. If this is a source to a different target then copy/move
			// search all connections
			foreach (DragDropConnection dragConn in this.ConnectionList)
			{
				if (processed) // already performed an operation
					break;
				// if source matches and target does not
				if (dragConn.MatchesSource(listBox) && !dragConn.MatchesTarget(listBox))
				{
					UpdateTarget(dragConn.Target, item, -1); // -1 to insert at end
					// if this is a move then remove from the sourcelist
					if (dragConn.Effects == DragDropEffects.Move)
						RemoveFromList(dragConn.source, item);
					processed = true;
				}
			}
			// 2. If this is a target-only listbox then remove it
			//      If the source / target relationship is move then
			//      add back to the original
			// In case of multiple registrations apply precedence rules
			foreach (DragDropConnection dragConn in this.ConnectionList)
			{
				if (processed) // already performed an operation eg. in previous loop
					break;
				// if target matches and source does not
				if (dragConn.MatchesTarget(listBox) && !dragConn.MatchesSource(listBox))
				{
					RemoveFromList(dragConn.Target, item);
					// if this is a move then remove from the sourcelist
					if (dragConn.Effects == DragDropEffects.Move)
						UpdateTarget(dragConn.Source, item, -1); // -1 to insert at end
					processed = true;
				}
			}
		}


	}

}